Learning TypeScript

Enhance Your Web Development Skills Using Type-Safe JavaScript

With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles.

Josh Goldberg

Learning TypeScript

by Josh Goldberg

Copyright © 2022 O’Reilly Media. All rights reserved.

Printed in the United States of America.

Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.

O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com.

Editors: Amanda Quinn and Rita Fernando

Production Editor: Beth Kelly

Copyeditor: FILL IN COPYEDITOR

Proofreader: FILL IN PROOFREADER

Indexer: FILL IN INDEXER

Interior Designer: David Futato

Cover Designer: Karen Montgomery

Illustrator: Kate Dullea

September 2022: First Edition

Revision History for the Early Release

2021-08-17: First Release

See http://oreilly.com/catalog/errata.csp?isbn=9781098110338 for release details.

The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Learning TypeScript, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.

The views expressed in this work are those of the author, and do not represent the publisher’s views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.

978-1-098-11026-0

[FILL IN]

Dedication

This book is dedicated to my incredible partner Mariah, who introduced me to the joy of adopting backyard cats and has regretted it ever since. Toot.

Preface

My journey to TypeScript was not a direct or quick one. I started off in school primarily writing Java, then C++, and like many new developers raised on statically typed languages, I looked down on JavaScript as “just” the sloppy little scripting language people throw onto websites.

My first substantial project in the language was a silly remake of the original Super Mario Bros. video game in pure HTML5/CSS/JavaScript and, typical of many first projects, was an absolute mess. In the beginning of the project I instinctively disliked JavaScript’s weird flexibility and lack of guardrails. It was only towards the end that I really began to respect JavaScript’s features and quirks: its flexibility as a language, its ability to mix and match small functions, and its ability to just work in user browsers within seconds of page load.

By the time I finished that first project, I had fallen in love with JavaScript.

Static analysis (tools to analyze your code without running it) such as TypeScript also gave me a queasy gut feeling at first. JavaScript is so breezy and fluid, I thought, why bog ourselves down with rigid structures and types?. Were we reverting back to the worlds of Java and C++ that I had left behind?

It took me all of ten minutes of struggling to read through my old, convoluted JavaScript code to understand how messy things could get without static analysis. The act of cleaning that code up showed me all the places where I would have benefitted from some structure. From that point on, I was hooked onto adding as much static analysis to my projects as I could.

It’s been nearly a decade since I first tinkered with TypeScript and I enjoy it as much as ever. The language is still evolving with new features and more useful than ever in providing safety and structure to JavaScript.

I hope that by reading Learning TypeScript you can learn to appreciate TypeScript the way I do: not just as a means to find bugs and typos --and certainly not a substantial change to JavaScript code patterns-- but as JavaScript with types: a beautiful system for declaring the way our JavaScript should work, and helping us stick to it.

Who Should Read This Book

If you have an understanding of writing JavaScript code, can run basic commands in a terminal, and are interested in learning about TypeScript, this book is for you.

Maybe you’ve heard TypeScript can help you write a lot of JavaScript with fewer bugs (true!) or document your code well for other people to read (also true!). Maybe you’ve seen TypeScript show up in a lot of job postings, or in a new role you’re starting.

Whatever your reason, as long as you come in knowing the fundamentals of JavaScript -variables, functions, closures/scope, and classes- this book will take you from no TypeScript knowledge to mastering the fundamentals and most important features of the language. By the end of this book you will understand:

The history and context for why TypeScript is useful on top of “vanilla’ JavaScript

How a type system analyzes and understands code

How to use development-only type annotations to inform the type system

How TypeScript works with IDEs to provide refactoring tools

And you will be able to:

Articulate the benefits of TypeScript and general characteristics of its type system

Add type annotations where useful in your code

Represent moderately complex types using TypeScript’s built-in inferences and new syntax

Use TypeScript to assist local development in refactoring code

Why I Wrote This Book

TypeScript is a wildly popular language in both industry and open source:

GitHub’s 2020 State of the Octoverse has it at the 4th top language, up from 7th in 2019 and 2018 and 10th in 2017.

StackOverflow’s 2020 Developer Survey has it at the world’s second most loved language (67.1% of users).

The 2020 State of JS survey has TypeScript at consistently having both high satisfaction and usage amounts as both a build tool and variant of JavaScript.

For frontend developers, TypeScript is well supported in all major UI libraries and frameworks, including Angular -which strongly recommends TypeScript-, Gatsby, Next.js, React, Svelte, and Vue. For backend developers, TypeScript generates JavaScript that runs natively in Node.js; Deno, a similar runtime by Node’s creator, emphasizes first class TypeScript support.

However, despite this plethora of popular project support, I was rather dissapointed by the lack of good introductory content online when I first learned the language. Many of the online documentation sources didn’t do a great job of explaining what a type system is or how to use it. They often assumed a great deal of prior knowledge in both JavaScript and strongly typed languages, or were written with only cursory code examples.

Not seeing an O’Reilly book with a cute animal cover introducing TypeScript was a dissapointment. I’m thrilled to be able to spend the time to really make a good, clear introduction to TypeScript for readers who aren’t already experts in its principles.

Navigating This Book

Learning TypeScript has two purposes:

You can read through it once to to understand TypeScript as a whole.

Later, you can refer back to it as a practical introductory TypeScript language reference.

This book ramps up from concepts to practical use across three general sections of chapters:

Part 1: Concepts: How JavaScript came to be, why TypeScript adds to it, and the basics of a type system as TypeScript creates it.

Part 2: Features: Fleshing out how the type system interacts with the major parts of JavaScript you’d work with when writing TypeScript code.

Part 3: Usage: Now that you understand the features that make up the TypeScript language, how to use them in real world situations to improve your code reading and editing experience.

I’ve thrown in one last chapter after that on “Cursory Miscellaneous Topics” to cover fancy TypeScript features. You won’t need to know them to consider yourself a TypeScript developer (and arguably most should be avoided whenever possible). They’re just nifty and interesting enough that I couldn’t resist giving them a mention.

Conventions Used in This Book

The following typographical conventions are used in this book:

Italic

Indicates new terms, URLs, email addresses, filenames, and file extensions.

Constant width

Used for program listings, as well as within paragraphs to refer to program elements such as variable or function names, databases, data types, environment variables, statements, and keywords.

Constant width bold

Shows commands or other text that should be typed literally by the user.

Constant width italic

Shows text that should be replaced with user-supplied values or by values determined by context.

Tip

This element signifies a tip or suggestion.

Note

This element signifies a general note.

Warning

This element indicates a warning or caution.

Using Code Examples

Supplemental material (code examples, exercises, etc.) is available for download at https://github.com/LearningTypeScript/learning-typescript.

If you have a technical question or a problem using the code examples, please send email to bookquestions@oreilly.com.

This book is here to help you get your job done. In general, if example code is offered with this book, you may use it in your programs and documentation. You do not need to contact us for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission.

We appreciate, but generally do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “Book Title by Some Author (O’Reilly). Copyright 2012 Some Copyright Holder, 978-0-596-xxxx-x.”

If you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at permissions@oreilly.com.

O’Reilly Online Learning

Note

For more than 40 years, O’Reilly Media has provided technology and business training, knowledge, and insight to help companies succeed.

Our unique network of experts and innovators share their knowledge and expertise through books, articles, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O’Reilly and 200+ other publishers. For more information, visit http://oreilly.com.

How to Contact Us

Please address comments and questions concerning this book to the publisher:

O’Reilly Media, Inc.

1005 Gravenstein Highway North

Sebastopol, CA 95472

800-998-9938 (in the United States or Canada)

707-829-0515 (international or local)

707-829-0104 (fax)

Email bookquestions@oreilly.com to comment or ask technical questions about this book.

For news and information about our books and courses, visit http://oreilly.com.

Find us on Facebook: http://facebook.com/oreilly

Follow us on Twitter: http://twitter.com/oreillymedia

Watch us on YouTube: http://www.youtube.com/oreillymedia

Acknowledgments

Chapter 1. From JavaScript to TypeScript

A Note for Early Release Readers

With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles.

This will be the 1st chapter of the final book.

If you have comments about how we might improve the content and/or examples in this book, or if you notice missing material within this chapter, please reach out to the author at me@joshuakgoldberg.com.

Before talking about TypeScript, we need to first understand where it came from: JavaScript!

History of JavaScript

JavaScript was originally designed in 10 days by Brendan Eich at Netscape in 1995 to be approachable and easy to use for websites. Developers have been poking fun at its quirks and perceived shortcomings ever since. I’ll cover some of them in the next section.

JavaScript has evolved tremendously since 1995, though! Its steering committee, TC39, has released new versions of the language consistently yearly since 2015 with new features that bring it in line with other modern dynamic languages. Impressively, even with regular new language versions, JavaScript has managed to maintain backwards compatibility for decades in varying environments, including browsers, embedded applcations, and server runtimes.

Today, JavaScript is a wonderfully flexible language with a lot of strengths. You and I should appreciate that while JavaScript has its quirks, it’s also helped enable the incredible growth of web pages and the internet.

Show me the perfect programming language and I’ll show you a language with no users.

Anders Hejlsberg, TSConf 2019

Vanilla JavaScript’s Pitfalls

Developers often refer to using JavaScript without any significant language extensions or frameworks as “vanilla”: referring to it being the familiar, original plain version. I’ll soon go over why TypeScript adds just the right flavor to overcome these particular major pitfalls, but it’s useful to understand just why they can be painful.

Costly Freedom

Many developers’ biggest gripe with JavaScript is unfortunately one of its key feature: JavaScript provides virtually no restrictions in how you structure your code. That freedom makes it a ton of fun to start a project in JavaScript! You can quickly write code in any which way and watch it magically work.

As you get to have more and more files, though, it becomes apparent how that freedom can be damaging. Take the following snippet, presented out of context from some fictional musical application:

function paintPainting(painter, painting) {
  return painter
    .prepare()
    .paint(painting, painter.ownMaterials)
    .finish();
}

Reading that code without any context, you can only have vague ideas how to use paintPainting. Perhaps if you’ve worked in the surrounding codebase you may recall that painter should be what’s returned by some getPainter function. You might even make a lucky guess that painting is a string.

Even if those assumptions are correct, though, later changes to the code may invalidate them. Perhaps painting is changed from a string to some other data type, or maybe one or more of the painter’s methods are renamed.

Other languages might refuse to let you run code if their compiler determines it would likely crash. Not so with JavaScript.

The freedom of code that makes JavaScript so fun becomes a real pain when you want safety in running your code.

Loose Documentation

Nothing exists in the JavaScript language specification to formalize describing what function parameters, function returns, variables, or other constructs in code are meant to be. Many developers have adopted a standard called JSDoc to describe functions and variables using block comments. The JSDoc standard describes how you might write documentation comments placed directly above constructs such as functions and variables, formatted in standard way. Here’s an example, again taken out of context:

/**
 * Performs a painter painting a particular painting.
 *
 * @param {Hero} painter
 * @param {string} painting
 * @returns {boolean} Whether painting was painted.
 */
function paintPainting(painter, painting) { /* ... */ }

JSDoc has key issues that often make it unpleasant to use in a large codebase:

It can be difficult during code refactors to find all the now-invalid JSDoc comments related to your changes

Nothing stops JSDoc descriptions from being wrong about code

Describing complex objects is unweildy, requiring multiple standalone comments to define types and their relationships

Weaker Developer Tooling

Because JavaScript doesn’t provide built-in ways to identify types, and the JSDoc standard is a suggestion that allows to code to easily diverge from comments, it can be very difficult to automate large changes to or gain insights about a codebase. JavaScript developers are often surprised to see features in typed languages such as C# and Java that allow developers to perform class member renamings or jump to the place an argument’s type was declared.

Note

You may protest that modern IDEs such as VS Code do provide some automated refactors to JavaScript. True, but: they use TypeScript to do so, and they’re not as reliable or as powerful as when refactoring well-defined TypeScript code.

TypeScript!

TypeScript was created internally at Microsoft in the early 2010s then released and open sourced in 2012. The head of its development is Anders Hejlsberg, notable for also having lead the development of the popular C# and Turbo Pascal languages. TypeScript is often described as a “superset of JavaScript” or “JavaScript with types”.

But what is TypeScript?

TypeScript is four things:

Programming language: a language that includes all the existing JavaScript syntax, plus new TypeScript-specific syntax for defining and using types.

Type checker: a program that takes in a set of files written in JavaScript and/or TypeScript, develops an understanding of all the constructs (variables, functions, …) created, and lets you know if it thinks anything is set up incorrectly.

Compiler: a program that runs the type checker, reports any issues, then outputs the equivalent JavaScript code.

Language services: a program that uses the type checker to tell editors such as VS Code how to provide helpful utilities to developers.

TypeScript in Action

Take a look at this code snippet:

const firstName  "Georgia";
const nameLength  firstName.length();
//                           ~~~~~~
// This expression is not callable.

The code is written in normal JavaScript syntax — I haven’t introduced TypeScript-specific syntax yet. If you were to run the TypeScript type checker on this code, it would use its knowledge that the length property of a string is a number -not a function- to give you the complaint shown in the comment.

If you were to paste that code into an editor with TypeScript support -meaning it runs the TypeScript language services for you-, it would be told by those language services to give you a little red squiggly under length indicating TypeScript’s displeasure with your code. Hovering over the squigglied code would give you the text of the complaint (Figure 1-1):

[picture]

Figure 1-1. TypeScript reporting an error on string length not being callable.

Being told of these simple errors in your editor as you type them is a lot more pleasant than waiting until a particular line of code happens to have been run in the app and throw an error.

Freedom Through Restriction

TypeScript allows us to specify what types may be provided as values for parameters and variables. Some developers find having to explicitly write out in your code how particular areas are supposed to work to be restrictive at first.

But! I would argue that being “restricted” in this way is actually a good thing! By restricting our code to only being able to be used in the ways you specify, TypeScript can give you confidence that changes in one area of code won’t break other areas of code that use it.

If, say, you change the number of required parameters for a function, TypeScript will let you know if you forget to update a place that calls the function.

In the following example, sayMyName was changed from taking in two parameters to taking one parameter, but the call to it with two strings wasn’t updated and so is triggering a TypeScript complaint:

// Previously: sayMyName(firstName, lastNameName) { ...
function sayMyName(fullName) {
  console.log(`You acting kind of shady, ain't callin' me ${fullName}`);
}

sayMyName("Beyoncé", "Knowles");
//                   ~~~~~~~~~
// Expected 1 arguments, but got 2.

Precise Documentation

Let’s look at a TypeScript-ified version of the paintPainting function from earlier. Although I haven’t yet gone over the specifics of TypeScript syntax for documenting types, the following snippet still hints at the great precision with which TypeScript can document code:

interface Painter {
  finish(): boolean;
  ownMaterials: Materials;
  paint(painting: string, materials: Material[]);
}

function paintPainting(painter: Painter, painting: string) { /* ... */ }

A TypeScript developer reading this code for the first time could understand that painter has at least three properties, two of which are methods. By baking in syntax to describe of the “shapes” of objects, TypeScript provides an excellent, precise method for describing how objects look.

Stronger Developer Tooling

TypeScript’s precise typings allow editors such as VS Code to gain a much deeper insight into your code, which they then use to surface intelligent suggestions as you type. These suggestions can be incredibly useful for development.

To start, if you’ve used VS Code to write JavaScript before, you might have noticed that it suggests “autocompletions” as you write code with built-in types of objects like strings (Figure 1-2):

[picture]

Figure 1-2. TypeScript providing autocompletion suggestions in JavaScript for a string.

When you add TypeScript’s type checker for understanding code, it can give you these rich suggestions even for code you’ve written. If start typing painter. in the paintPainting function, TypeScript would take its knowledge that the painter parameter is of type Painter and the Painter type has the following members (Figure 1-3):

[picture]

Figure 1-3. TypeScript providing autocompletion suggestions in TypeScript for a custom type.

Snazzy!

What TypeScript Is Not

Now that you’ve seen how awesome TypeScript is, before you give it a test drive, I have to warn you about some limitations. It’s still a very fantastic piece of technology but it’s important to check expectations regarding what TypeScript is, and isn’t. As with every tool, it excels at some areas and has limitations in others.

A Remedy for Bad Code

TypeScript helps you structure your JavaScript but other than enforcing type safety it doesn’t enforce any opinions on what that structure should look like.

Good!

TypeScript is a language that everyone is meant to be able to use, not an opinionated framework with a target audience.

If anybody tries to tell you that TypeScript forces you to use classes, or makes it hard to write good code, or whatever code style complaints are out there, give them a stern look and tell them to pick up a copy of Learning TypeScript. TypeScript does not enforce code style opinions such as whether to use classes or functions.

Extensions to JavaScript (Mostly)

TypeScript’s design goals explicitly state that it should:

Align with current and future ECMAScript proposals.

Preserve runtime behavior of all JavaScript code.

TypeScript does not change how JavaScript works at all. Its creators have tried very hard to avoid adding new code features that would add to or conflict with JavaScript. Such a task is the domain of TC39, the technical committee that works on ECMAScript itself.

There are a few older features in TypeScript that were added many years ago to reflect common use cases in JavaScript code. Most of those features are either relatively uncommon, have fallen out of favor and are only covered briefly in Appendix ??. I recommend staying away from them in most cases.

Slower Than JavaScript

Sometimes on the internet, you might hear some opinionated developers complain that TypeScript is slower than JavaScript at runtime. That claim is generally misleading and inaccurate: TypeScript doesn’t introduce any runtime complexity to code, so running code compiled from TypeScript can’t be inherently slower!

TypeScript does, however, add some time to building your code. TypeScript code must be compiled down to JavaScript before most environments, such as browsers and Node.js, will run it. Most build pipelines are generally set up so that the performance hit is negligible, and expensive TypeScript-only operations are done separately from truly building code to be run.

Warning

Even projects that seemingly allow running TypeScript code directly, such as ts-node and Deno, themselves internally convert TypeScript code to JavaScript before running it.

Finished Evolving

The web is nowhere near finished evolving, and thus neither is TypeScript. The TypeScript language is constantly receiving bug fixes and feature additions to match the ever-shifting needs of the web community. The basic tenets of TypeScript you’ll learn in this book will remain about the same, but error messages, fancier features, and editor integrations will improve over time.

In fact, while this edition of the book was published with TypeScript version 4.3.5 as the latest, by the time you started reading it we can be certain a newer version has been released. Some of the TypeScript error messages in this book might even already be out of date! Thanks, Anders.

Getting Started in the TypeScript Playground

You’ve read a good amount about TypeScript by now. Let’s get you writing it!

The main TypeScript website includes a “playground” editor at https://www.typescriptlang.org/play. You can type code into the main editor and see many of the same editor suggestions you would working with TypeScript locally in a= full IDE.

Most of the snippets in this book are intentionally small and self-contained enough that you could type them out in the playground and tinker with them for fun.

Compiling Syntax

TypeScript’s compiler allows us to input TypeScript syntax, have it type checked, and get the equivalent JavaScript emitted. As a convenience, the compiler will also take modern JavaScript syntax and compile it down into its older ECMAScript equivalents.

If you were to paste this TypeScript code into the playground:

const warrior = "Kahina";
console.log({ warrior });

The playground would show you on the right-hand-side of the screen that this would be the equivalent JavaScript output by the compiler:

"use strict";
var warrior = "Kahina";
console.log({ warrior: warrior });

Note

Many JavaScript projects use a tool called Babel (https://babeljs.io) for this same purpose. Babel does have great TypeScript tooling, though I don’t cover it until Chapter ??.

Getting Started Locally

To install the latest version of TypeScript globally on your computer, run the following command:

npm i -g typescript

Now, you’ll be able to run TypeScript on the command-line with the tsc (TypeScript Compiler) command. Try it with the --version command to make sure it’s set up properly.

tsc --version

It should print out something like Version X.Y.Z — whichever version is current as of you installing TypeScript.

Running Locally

Now that TypeScript is installed, let’s have you set up a folder locally to run TypeScript on code. Create a folder somewhere on your computer - for my commands, I’ll assume the path /code/learning-typescript - and run this command to create a new tsconfig.json configuration file:

tsc --init

A tsconfig.json file declares the settings that TypeScript uses when analyzing your code. Most of the options in that file aren’t going to be relevant to you in this book (there are a lot of uncommon edge cases in programming that the languages need to account for!). I’ll cover them in Chapter ??.

The important feature is that now you can run tsc to tell TypeScript to compile all the files in that folder.

Try adding a file named index.ts with the following contents:

console.blub("Hello world!");

Then, run tsc.

tsc

You should get an error that looks roughly like:

index.ts:1:9 - error TS2339: Property 'blub' does not exist on type 'Console'.

1 console.blub("Hello world!");
          ~~~~

Found 1 error.

Indeed, blub does not exist on the console. What was I thinking?

Before you fix the code to appease TypeScript, note that tsc created an index.js for you with contents including the console.blub.

Note

This is an important concept to reflect on: even though there was a type error in our code, the syntax was still completely valid. The TypeScript compiler was able to produce valid JavaScript from the input file’s valid syntax regardless of the type error.

Correct the code in index.ts to call console.log and run tsc again. There should be no complaints in your terminal, and the index.js file should now contain updated output code.

"use strict";
console.log("Hello, world!");

Editor Features

Another benefit of creating a tsconfig.json file is that when editors are opened to a particular folder, they will now recognize that folder as a TypeScript project. For example, if you open VS Code in the same folder through its GUI or with its terminal command:

code .

(alternately, code /code/learning-typescript)

The settings it uses to analyze your TypeScript code will respect whatever’s in the tsconfig.json file it sees in that folder.

As an exercise, go back through the code snippets in this chapter and type them in your editor. You should see dropdowns suggesting completions for names as you type them, especially for members such as the log on console.

Very exciting: you’re using the TypeScript language services to help yourself write code! You’re on your way to being a TypeScript developer!

Tip

VS Code comes with great TypeScript support. Many TypeScript and/or web developers use VS Code as their primary editor — myself included. You don’t have to use it for TypeScript, but I do recommend it for at least trying out TypeScript while reading through this book.

Summary

In this chapter, you read up on the context for some of JavaScript’s main weaknesses and how TypeScript helps with:

The history of JavaScript

JavaScript’s pitfalls: costly freedom, loose documentation, and and weaker developer tooling

TypeScript’s advantages: freedom through restriction, precise documentation, and stronger developer tooling

What TypeScript is not: a remedy for bad code, extensions to JavaScript (mostly), slower than JavaScript, or finished evolving

Getting started writing TypeScript code on the TypeScript playground and locally on your computer

Chapter 2. The Type System

A Note for Early Release Readers

With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles.

This will be the 2nd chapter of the final book.

If you have comments about how we might improve the content and/or examples in this book, or if you notice missing material within this chapter, please reach out to the author at me@joshuakgoldberg.com.

I talked briefly in Chapter 1 about the existence of a “type system” in TypeScript, where its type checker looks at your code, understands how it’s meant to work, and lets you know where you might have messed up. But how does a type system work, really?

What’s in a Type?

A “type” is a description of what a JavaScript value shape might be. By shape I mean how a value looks and feels: its properties and methods, and what the built-in typeof operator would describe it as. The most basic types in TypeScript correspond to the seven basic primitives in JavaScript:

bigint: 0n, 2n, -4n, …

boolean: true or false

null

number: 0, 2, -4, …

string: "", "Hi!", `"ߑ뢠, …

symbol: Symbol(), Symbol("hi"), …

undefined

For example, when you create a variable:

let singer = "Aretha";

TypeScript can figure out, or infer, that the singer variable is of type string.

For each of these values, TypeScript understands the type of the value to be one of the seven basic primitives:

1337n; // bigint
true; // boolean
null; // null
1337; // number
"Louise"; // string
Symbol("Franklin"); // Symbol
undefined; // undefined

TypeScript is also smart enough to figure out the type of a variable whose starting value is computed. In this example, TypeScript knows that the ternary expression always results in a string, so the bestSong value is a string:

let bestSong = Math.random() > 0.5 ? "Chain of Fools" : "Respect";

Type Inferences in Detail

At its core, TypeScript’s type system works by:

Reading in your code and understanding all the types and values in existence

For each object, seeing what type its initial declaration indicates it may contain

For each object, seeing all ways it’s used later on

Complaining to the user if an object’s usage doesn’t match with its type

Let’s walk through that type inference system in detail.

Take the following snippet, in which TypeScript is emitting a type error about a member variable being erroneously called as a function:

let firstName = "Cleopatra";
firstName.length();
//        ~~~~~~
//  This expression is not callable.
//    Type 'Number' has no call signatures

TypeScript came to that complaint by, in order:

Reading in the code and understanding there to be one object: firstName

Concluding that firstName is of type string its initial value is a string, "Cleopatra"

Seeing that the code is trying to access a .length member of firstName and call it like a function

Complaining that the .length member of a string is a number, not a function (it can’t be called like a function)

Understanding TypeScript’s type inference is an important skill for understanding TypeScript code. Code snippets in this chapter and through the rest of this book will display more and more complex types that TypeScript will be able to infer from code.

Kinds of Errors

While writing TypeScript, the two kinds of “errors” you’ll come across most frequently are:

Syntax: blocking TypeScript from being converted to JavaScript.

Type: something mismatched has been detected by the type checker.

It’s useful to be able to understand the differences between the two.

Syntax Errors

Syntax errors are when TypeScript detects incorrect syntax that it cannot understand as code. These block TypeScript from being able to properly generate output JavaScript from your file. Depending on the tooling and settings you’re using to convert your TypeScript code to JavaScript, you might to still get some kind of JavaScript output. If you do, it likely won’t look like what you expect.

This input TypeScript has a syntax error for an unexpected let:

let let wat;

Its compiled output in TypeScript, depending on the language version, may look something like:

let let, wat;

Type Errors

Type errors occur when your syntax is valid but the TypeScript type checker has detected an error with the program’s types. These do not block TypeScript syntax from being converted to JavaScript. They do, however, often indicate something will crash or behave unexpectedly if your code is allowed to run.

You saw this earlier with the console.blub example, where it is syntactically valid code but TypeScript can detect it will likely crash when run:

console.blub("Hello world!");

Example 2-1.

Some projects are configured to block running code during development until all TypeScript type errors -not just syntax- are fixed. Many developers, myself included, generally find this to be annoying and unnecessary. Most projects have a way to disable it, such as with the tsconfig.json file covered in Chapter ??.

Assignability

TypeScript reads variables’ initial values to determine what type those variables are allowed to be. If it later sees an assignment of a new value to that variable, it will check if that assignment value’s type is the same as the variable’s.

TypeScript would be fine with later assigning a different value of the same type to a variable. If a variable is, say, initially a string value, later assigning it another string would be fine:

let firstName = "Carole";
firstName = "Louise";

When a variable is declared with an initial value, it’s considered to be of that initial value’s type — and must always be assigned values of that type. If TypeScript sees an assignment of a different type, it will give us type error. We couldn’t, say, initially declare a variable with a string value and then later on put in a number:

let firstName = "King";
firstName = 1337;
// Error: Type 'number' is not assignable to type 'string'.

TypeScript’s checking of whether a value is allowed to be provided to a function call or variable is called “assignability”: whether that value is assignable to the location it’s passed to. This will be an important term in later chapters as we compare more complex objects.

Type Annotations

Sometimes a variable doesn’t have an initial value for TypeScript to read. TypeScript won’t attempt to figure out what value those variables are from later usage. It’ll consider them to be implicitly the any type: a type indicating that it could be anything in the world.

let rocker;
rocker = "Joan Jett";

Allowing variables to be of type any defeats the purpose of TypeScript’s type checking! TypeScript works best when it knows what types your values are meant to be. Most of TypeScript’s type checking can’t be applied to any typed values because they don’t have known types to be checked.

Although TypeScript can still emit JavaScript code despite an implicit any, it will yell at you about them in the form of type errors. Chapter ?? will cover how to configure TypeScript’s implicit any complaints.

Instead, TypeScript provides a syntax for declaring the type of a variable, using what’s called a type annotation. A type annotation is placed after the name of a variable and includes a colon followed by the name of a type.

let rocker: string;
rocker = "Joan Jett";

These type annotations are unique to TypeScript. If you run tsc to compile TypeScript source code to JavaScript, they’ll be erased. For example:

// output .js file
let rocker;
rocker = "Joan Jett";

Example 2-2.

TypeScript contains an assortment of new pieces of syntax such as these type annotations that exist only in the type system. Nothing that exists only in the type system gets copied over into emitted JavaScript.

Unnecessary Type Annotations

Type annotations allow us to provide information to TypeScript that it wouldn’t have been able to glean on its own. You could use them on variables that have immediately inferable types, but you wouldn’t be telling TypeScript anything it doesn’t already know:

let firstName: string = "Tina";
//           ~~~~~~~~ Does not change the type system...

If you do add a type annotation to a variable with an initial value, TypeScript will check that it matches the type of the variable’s value.

The following firstName is declared to be of type string but its initializer is the number 42, which is TypeScript sees as an incompatibility:

let firstName: string = 42;
//  ~~~~~~~~~
//  Error: Type 'number' is not assignable to type 'string'.

Example 2-3.

Most developers -myself included- generally prefer not to add type annotations in places where they don’t change anything. Having to manually write out type annotations can be cumbersome — especially when they change, and for the complex types I’ll show you later in this book.

Type Shapes

TypeScript doesn’t only check that the values assigned to variables match their original types: it also knows what member properties should exist on objects. If you attempt to access a property of a variable, TypeScript will make sure that property is known to exist on that variable’s type.

Suppose we declare a rapper variable of type string. Later on, when we use that string variable, operations that TypeScript knows work on strings are allowed:

let rapper = "Queen Latifah";
rapper.length; // ok

Operations that TypeScript doesn’t know to work on strings will not be allowed:

rapper.asdfqwerty;
//     ~~~~~~~~~~
// Property 'asdfqwerty' does not exist on type 'string'.

Types can also be more advanced shapes, most notably complex objects. In the following snippet, TypeScript knows the birthNames object doesn’t have a rihanna key and complains:

let cher = {
  firstName: "Cherilyn",
  lastName: "Sarkisian",
};

cher.middleName;
//   ~~~~~~~~~~
//   Property 'middleName' does not exist on type
//   '{ firstName: string; lastName: string; }'.

Chapter 7 will describe more on objects and object types.

Summary

In this chapter, you saw how TypeScript’s type system works at its core:

What a “type” is and what types are recognized by TypeScript

Inferred variable types and variable assignability

Type annotations to explicitly declare variable types

Object member checking on type shapes

How type complaints compare to syntax complaints

Chapter 3. Unions and Narrowing

A Note for Early Release Readers

With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles.

This will be the 3rd chapter of the final book.

If you have comments about how we might improve the content and/or examples in this book, or if you notice missing material within this chapter, please reach out to the author at me@joshuakgoldberg.com.

Chapter 2 covered the concept of the “type system” and how it can read values to understand the types of variables. Before I talk about all the various syntax features of TypeScript, I’d like to introduce two key concepts that TypeScript works with to make inferences on top of those values:

Unions: When a value is known to be one of two or more possible types.

Narrowing: Deducing that a value to not be one or more possible types.

Put together, unions and narrowing are a powerful concept that allow TypeScript to make informed inferences on your code many other mainstream languages cannot.

Union Types

Take this mathematician variable:

let mathematician = Math.random() > 0.5
    ? 9001
    : "Mark Goldberg";

What type is mathematician?

It’s neither only number nor only string, even though those are both potential values. mathematician can be either number or string. This kind of “either or” type is called a “union”. Union types are a wonderful concept that let us handle code cases where we don’t know exactly which type a variable is, but do know it’s one of a two or more options.

TypeScript represents union types to us using the | (pipe) operator between the possible values. The above mathematician type is thought of as number | string.

[picture]

Figure 3-1. TypeScript reporting the mathematician variable as being type string | number.

Declaring Union Types

Union types are an example of a time when it might be useful to give an explicit type annotation for a variable even though it has an initial value. In this example, thinker starts off false but is known to potentially contain a string instead:

let thinker: boolean | string = false;

if (Math.random() > 0.5) {
    thinker = "Susanne Langer";
}

Union Properties

When a value is known to be a union type, TypeScript will only allow you to access member properties that exist on all possible types in the union. It will give you a type checking error if you try to access a type that doesn’t exist on all possible types.

In the following snippet, physicist is of type number | string, so while .toString() exists in both types and is allowed to be used, .toUpperCase() and .toFixed() each missing on one of the union types and therefore are not allowed:

let physicist = Math.random() > 0.5
    ? "Marie Curie"
    : 84;

physicist.toString(); // Ok

physicist.toUpperCase();
//        ~~~~~~~~~~~
//        Error: Property 'toUpperCase' does not exist on type 'string | number'.
//          Property 'toUpperCase' does not exist on type 'number'.

physicist.toFixed();
//        ~~~~~~~
//        Error: Property 'toFixed' does not exist on type 'string | number'.
//          Property 'toFixed' does not exist on type 'string'.

If you want to use a member of a union typed value that only exists on a subset of the potential types, your code will need to indicate to TypeScript that the value is one of those more specific types: a process called narrowing.

Example 3-1.

The order of a union type declaration does not matter.

You can write boolean | number or number | boolean and TypeScript will treat both the exact same.

Narrowing

Narrowing is when TypeScript deduces from your code that a value is of a more specific type than what it was defined, declared, or previously inferred as. Once TypeScript knows that a type is more narrow than previously known, it will allow you to treat it like that more specific type.

I’ll use the remainder of this chapter to show you the common ways TypeScript can deduce type narrowing from your code.

Assignment Narrowing

If you directly assign a value to a variable, TypeScript will understand the variable to have been narrowed to that value’s type:

let biochemist: number | string;

biochemist = "Katalin Kariko";

biochemist.toUpperCase(); // Ok: string

biochemist.toFixed();
//         ~~~~~~~
//         Error: Property 'toFixed' does not exist on type 'string'.

Assignment narrowing comes into play when a variable is given an explcit union type annotation and an initial value, too. TypeScript will understand that while the variable may later receive a value of any of the union typed values, it starts off as only the type of its in itial value.

In the following snippet, inventor is declared as type number | string, but TypeScript knows it’s immediately narrowed to a string from its initial value:

let inventor: number | string = "Hedy Lamarr";

inventor.toUpperCase(); // Ok: string

inventor.toFixed();
//       ~~~~~~~
//       Error: Property 'toFixed' does not exist on type 'string'.

Conditional Checks

One of the simplest ways to get TypeScript to narrow a variable’s value without assigning the value yourself is to write an if statement checking the variable for being equal to a known value. TypeScript is smart enough to understand that inside the body of that if statement, the variable must be the same type as the known value.

let scientist = Math.random() > 0.5
    ? "Rosalind Franklin"
    : 51;

if (scientist === "Rosalind Franklin") {
    scientist.toUpperCase(); // Ok: string
}

physicist.toUpperCase();
//        ~~~~~~~~~~~
//        Error: Property 'toUpperCase' does not exist on type 'string | number'.
//          Property 'toUpperCase' does not exist on type 'number'.

TypeScript also recognizes the typeof operator in narrowing down variable types:

if (typeof scientist === "string") {
    scientist.toUpperCase(); // Ok: string
}

Logical negations from ! and else statements work as well:

if (!(typeof scientist === "string")) {
    scientist.toFixed(); // Ok: number
} else {
    scientist.toUpperCase(); // Ok: string
}

The above can be rewritten with a ternary statement, which is also supported for type narrowing:

typeof scientist === "string"
    ? scientist.toUpperCase() // Ok: string
    : scientist.toFixed(); // Ok: number

Narrowing with conditional logic shows TypeScript’s type checking logic mirroring good JavaScript coding patterns. If a variable might be one of several types, you’ll generally want to check its type for being what you need. TypeScript is forcing us to play it safe with our code. Thanks, TypeScript!

TypeScript’s type checker recognizes several more forms of narrowing that we’ll see in later chapters.

Summary

In this chapter, I went over how TypeScript can deduce more specific (“narrow”) type information from how your code is structured:

How a union types represent values that could be one of two or more types

Explicitly indicating union types with type annotations

How type narrowing

Triggering type narrowing Conditional checks causing TypeScript to narrow specific values

Chapter 4. Literals

A Note for Early Release Readers

With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles.

This will be the 4th chapter of the final book.

If you have comments about how we might improve the content and/or examples in this book, or if you notice missing material within this chapter, please reach out to the author at me@joshuakgoldberg.com.

Chapter 3 introduced the concept of union types and narrowing, allowing you to work with values that may be two or more potential types. I’ll now go the opposite direction by introducing literal types: more specific versions of primitive types.

Literal Types

Take this philosopher variable:

const philosopher = "Hypatia";

What type is `philosopher?

At first glance, you might say string — and you’d be correct. philosopher is indeed a string.

But! philosopher is not just any old string. It’s specifically the value "Hypatia". Therefore, the philosopher variable’s type is technically "Hypatia" itself as well.

Such is the concept of a literal: a value that is known to be a distinct instance of a primitive.

If you declare a variable as const and directly give it a literal value, TypeScript will understand the variable to be that literal value as a type. This is why, when you hover a mouse over a const variable in an IDE such as VS Code, it will show you the variable’s type as that literal (Figure 4-1) instead of the more general primitive (Figure 4-2).

[picture]

Figure 4-1. TypeScript reporting a const variable as being specifically its literal type.

[picture]

Figure 4-2. TypeScript reporting a let variable as being generally its primitive type.

You can think of each primitive type as a union of every possible matching literal value. Most primitive types have a theoretically infinite number of literal types, but not all. Out of some common ones you’ll find in typical TypeScript code:

boolean: Just true | false

null and undefined: both just have one literal value, themselves

number: 0 | 1 | 2 | ... | 0.1 | 0.2 | ...

string: "" | "a" | "b" | "c" | ... | "aa" | "ab" | "ac" | ...

Union type annotations can mix and match between literals and primitives. A representation of a lifespan, for example, might be represented by any number or one of a couple known edge cases:

let lifespan: number | "ongoing" | "unknown";

lifespan = 89; // Ok
lifespan = "ongoing"; // Ok

lifespan = true;
// Error: Type 'true' is not assignable to
// type 'number | "ongoing" | "unknown"'

Literal Assignability

Literal types are subsets of their general primitive types. A value that is known to be a different literal, or only as a general primitive, may not be assigned to a variable that only allows a different literal.

In this example, specificallyAda is declared as being of the literal type "Ada", so the types "?!" and string are not assignable to it:

let specificallyAda: "Ada";

specificallyAda = "Ada"; // Ok

specificallyAda = "?!";
// Error: Type '"?!"' is not assignable to type '"Ada"'.

let someString = "";

specificallyAda = someString;
// Error: Type 'string' is not assignable to type '"Ada"'.

Literal types are, however, allowed to be assigned to their corresponding primitive types. A specific literal string is still generally a string, after all.

In this code example, the value ":)", which is of type ":)", is being assigned to the someString variable previously inferred to be of type string.

someString = ":)";

Who would have thought a simple variable assignment would be so theoretically intense?

Strict Null Checking

The power of narrowed unions with literals is particularly visible when working with potentially undefined valuables, an area of type systems known as strict null checking. TypeScript is part of a surge of modern programming languages that utilizes strict null checking to fix the dreaded “billion dollar mistake”.

The Billion Dollar Mistake

The “billion dollar mistake” is a catchy industry term for many type systems allowing values such as null to be used in places that require a different type. In languages without strict null checking, code like example that assign null to a string is allowed:

const firstName: string = null;

If you’ve previously worked in a typed language such as C++ or Java that suffers from the billion dollar mistake, it may be surprising to you that some languages don’t allow such a thing. If you’re never worked in a language with the billion dollar mistake before, it may be surprising that some languages do allow such a thing in the first place!

In the words of the developer who coined the phrase:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965… This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Tony Hoare, 2009

The TypeScript compiler has an --strictNullChecks option to toggle whether strict null checking is enabled. With the option disabled, the following code is considered totally type safe:

let nameMaybe = Math.random()
    ? "Tony Hoare"
    : undefined;

nameMaybe.toLowerCase();

Roughly speaking, disabling strict null checking adds | null | undefined to every type in your code, thereby allowing any variable to receive null or undefined.

TypeScript best practice is generally to enable strict null checking. Doing so helps prevent crashes and eliminates the billion dollar mistake.

With strict null checking enabled, TypeScript sees the potential crash in the code snippet:

let nameMaybe = Math.random()
    ? "Tony Hoare"
    : undefined;

nameMaybe.toLowerCase();
// Error: Object is possibly 'undefined'.

Truthiness Narrowing

TypeScript can also narrow a variable’s type from a truthiness check if only some of its potential values may be truthy. In the following snippet, cytogeneticist is of type string | undefined, and because undefined is always falsy, TypeScript can deduce that it must be of type string within the if statement’s body:

let cytogeneticist = Math.random() > 0.5
    ? "Barbara McClintock"
    : undefined;

if (cytogeneticist) {
    cytogeneticist.toUpperCase(); // Ok: string
}

cytogeneticist.toUpperCase();
// Error: Object is possibly 'undefined'.

Logical operators that perform truthiness checking work as well, namely ?. and &&:

cytogeneticist.toUpperCase(); // Ok: string
cytogeneticist && cytogeneticist.toUpperCase(); // Ok: string

Unfortunately, truthiness checking doesn’t go the other way for primitive values. If all we know about a string | undefined value is that it’s falsy, that doesn’t tell us whether it’s "" or undefined.

Here, placeholder is of type false | string, and while it can be narrowed down to just string in the if statement body, the else statement body knows it can still be a string if it’s "":

let biologist = Math.random() > 0.5 && "Rachel Carson";

if (!biologist) {
    biologist.toUpperCase(); // Not ok: type `false | string`
           // ~~~~~~~~~~~
           // Error: Property 'toUpperCase' does not
           // exist on type 'string | false'.
           //   Property 'toUpperCase' does not
           //   exist on type 'false'.
}

Implicit Union Type Truthiness

If you declare a variable with a type and no initial value, TypeScript is smart enough to understand that the variable is undefined until a value is assigned. It will even give you a specialized error message letting you know if you try to access a member before assigning a value:

let mathematician: string;

mathematician.length;
// Error: Variable 'mathematician' is used before being assigned.

mathematician = "Mark Goldberg";
mathematician.length; // Ok

Summary

In this chapter, you saw how TypeScript handles specific “literal” values within primitives:

The difference between const literals and let primitives

The “Billion Dollar Mistake” and how TypeScript handles strict null checking

Using explicit | undefined to represent values that might not exist

Implicit | undefined for unassigned variables

Preface

Who Should Read This Book

Why I Wrote This Book

Navigating This Book

Conventions Used in This Book

Using Code Examples

O’Reilly Online Learning

How to Contact Us

Acknowledgments

1. From JavaScript to TypeScript

History of JavaScript

Vanilla JavaScript’s Pitfalls

Costly Freedom

Loose Documentation

Weaker Developer Tooling

TypeScript!

TypeScript in Action

Freedom Through Restriction

Precise Documentation

Stronger Developer Tooling

What TypeScript Is Not

Getting Started in the TypeScript Playground

Compiling Syntax

Getting Started Locally

Running Locally

Editor Features

Summary

2. The Type System

What’s in a Type?

Type Inferences in Detail

Kinds of Errors

Assignability

Type Annotations

Unnecessary Type Annotations

Type Shapes

Summary

3. Unions and Narrowing

Union Types

Declaring Union Types

Union Properties

Narrowing

Assignment Narrowing

Conditional Checks

Summary

4. Literals

Literal Types

Literal Assignability

Strict Null Checking

The Billion Dollar Mistake

Truthiness Narrowing

Implicit Union Type Truthiness

Summary